# -*- coding: utf-8 -*-
#--------------------------------------------------------------------------
# Copyright (c) 2009 Barry Schwartz
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

import datetime
import decimal
import os
import mmap
import pyPdf
import random
import re
import shutil
import subprocess
import sys
import tempfile
import traceback
import zlib

from . import (
    config,
    file_type,
    image,
    kpathsea,
    pdf_crypto,
    pdfobj,
    temp_dir,
)

from .pdfobj import pt_to_bp

#from kt import config, file_type, image, kpathsea, pdf_crypto, pdfobj, temp_dir
#from kt.pdfobj import pt_to_bp

#--------------------------------------------------------------------------

class compression_params:
    level = 6

class font_params:
    no_hinting = False

def identity_function(x):
    return x

#--------------------------------------------------------------------------

def py_write_pdf(job, pages):

    try:
        # TODO: Add timezone information. However, even if timezone
        # information is left out, the PDF Reference says this time should
        # be set according to the local clock.
        creation_date = datetime.datetime.now().strftime('D:%Y%m%d%H%M%S')

        with open(job['output_file'], 'wb') as f:

            dictionaries = {'Info' : {}, 'Catalog' : {}}
            pdf = pdf_writer(f, offset = 0, dictionaries = dictionaries)
            pdf.write(pdf_header())

            page_list = []
            xobjects = {}
            fonts = {}

            id_string = pdf_crypto.compute_id_string(file_location = job['output_file'])
            encrypt = make_encrypt_dict(id_string)

            pdf.dictionaries['Info']['Creator'] = pdfobj.pdf_string(b'Kompostilo Typesetter')
            pdf.dictionaries['Info']['Producer'] = pdfobj.pdf_string(b'Kompostilo Typesetter')
            pdf.dictionaries['Info']['CreationDate'] = pdfobj.pdf_string(creation_date.encode('ASCII'))

            for p in pages:
                page_width = p['p_width']
                page_height = p['p_height']
                page_box = p['p_contents']
                page = build_page(pdf, fonts, xobjects, page_box, page_width, page_height)
                page_list.append(page)

            for font in list(fonts.values()):
                write_font(pdf, font)

            pdf.dictionaries['Catalog']['Type'] = pdfobj.pdf_name('Catalog')
            pdf.dictionaries['Catalog']['Pages'] = pdfobj.pdf_reference(make_page_tree(page_list))

            catalog_indirect = pdfobj.indirect(pdf.dictionaries['Catalog'], pdfobj.pdf_dict)
            info_indirect    = pdfobj.indirect(pdf.dictionaries['Info'], pdfobj.pdf_dict)

            flush_objects(pdf)
            xref_offset = pdf.offset
            write_xref(pdf)
            pdf.write(trailer_dict(root = pdfobj.pdf_reference(catalog_indirect),
                                   size = len(pdfobj.object_list) + 1,
                                   info = pdfobj.pdf_reference(info_indirect),
                                   id_string = id_string,
                                   encrypt = encrypt))
            pdf.write(startxref(xref_offset))

    except Exception as exc:
#    except Exception, exc:
        print('PYTHON EXCEPTION:')
        print(traceback.print_exc())
        raise exc

#--------------------------------------------------------------------------

def make_tree(node_list, max_node_size = 10):
    while max_node_size < len(node_list):
        n = max_node_size - 1
        quotient = len(node_list) // n
        remainder = len(node_list) % n
        nodes = []
        i = 0
        for j in range(0, remainder):
            nodes.append(node_list[i : i + n + 1])
            i = i + n + 1
        for j in range(remainder, quotient):
            nodes.append(node_list[i : i + n])
            i = i + n
        node_list = nodes
    return node_list

def traverse(parent):
    p = { 'Type' : pdfobj.pdf_name('Pages') }
    p_indirect = pdfobj.indirect(p, pdfobj.pdf_dict)
    leaf_count = 0
    kids = []

    for k in parent:
        if isinstance(k, dict):
            k_indirect = pdfobj.indirect(k, pdfobj.pdf_dict)
            k['Parent'] = pdfobj.pdf_reference(p_indirect)
            kids.append(pdfobj.pdf_reference(k_indirect))
            leaf_count = leaf_count + 1
        else:
            (k_indirect, dictionary, count) = traverse(k)
            dictionary['Parent'] = pdfobj.pdf_reference(p_indirect)
            kids.append(pdfobj.pdf_reference(k_indirect))
            leaf_count = leaf_count + count

    p['Kids']  = pdfobj.pdf_array(kids)
    p['Count'] = pdfobj.pdf_int(leaf_count)

    return (p_indirect, p, leaf_count)

def make_page_tree(pages, max_node_size = 10):
    tree = make_tree(pages, max_node_size)
    (tree_indirect, ignored_, ignored_) = traverse(tree)
    return tree_indirect

#--------------------------------------------------------------------------

class PDFObjectError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class pdf_writer(object):

    def __init__(self, f = sys.stdout, offset = 0, dictionaries = {}):
        self.f = f
        self.offset = offset
        self.compressor = None
        self.buf = None
        self.stack = []
        self.dictionaries = dictionaries

    def write(self, s):
        if self.compressor:
            block_size = 5000
            self.buf = self.buf + s
            while block_size <= len(self.buf):
                data = self.compressor.compress(self.buf[:block_size])
                self.f.write(data)
                self.buf = self.buf[block_size:]
                self.offset = self.offset + len(data)
        else:
            self.f.write(s)
            self.offset = self.offset + len(s)

    def write_string(self, s):
        self.write(s.encode('UTF-8'))

    def embed_file(self, file_name, block_size = 8192):
        assert self.compressor == None # FIX: compression not yet supported.
        total_size = 0
        with open(file_name, 'rb') as source:
            data = source.read(block_size)
            while len(data) != 0:
                self.f.write(data)
                total_size = total_size + len(data)
                data = source.read(block_size)
        self.offset = self.offset + total_size

    def begin_compression(self, level):
        self.compressor = zlib.compressobj(level)
        self.buf = ""

    def end_compression(self, level):
        data = self.compressor.compress(self.buf)
        self.f.write(data)
        self.offset = self.offset + len(data)
        data = self.compressor.flush()
        self.f.write(data)
        self.offset = self.offset + len(data)
        self.compressor = None
        self.buf = None

    def flush(self):
        if not self.compressor:
            self.f.flush()

    def push(self, f, offset = 0):
        self.f.flush()
        self.stack.append((self.f, self.offset, self.compressor, self.buf))
        self.f = f
        self.offset = offset
        self.compressor = None
        self.buf = None

    def pop(self):
        self.f.flush()
        f = self.f
        (self.f, self.offset, self.compressor, self.buf) = self.stack.pop()
        return f

    def register_object(self, contents, obj, obj_no):
        pdf_obj = construct_pdf_object(obj, self, contents, object_no = obj_no)
        indirect_object = pdfobj.object_list[obj_no - 1]
        indirect_object['data'] = pdf_obj
        indirect_object['string_maker'] = identity_function

    def update_dictionary(self, contents, dictionary_name, new_entries):
        if new_entries[0] != 'PDF_Dict':
            raise PDFObjectError('Expected a PDF_Dict, but got a ' + new_entries[0])
        if dictionary_name not in self.dictionaries:
            self.dictionaries[dictionary_name] = {}
        update_pdf_dictionary(self.dictionaries[dictionary_name], self,
                              contents, new_entries[1])

def update_pdf_dictionary(dictionary, pdf, contents, new_entries,
                          compression_allowed = True,
                          encryption_allowed = True):
    for (key, value) in list(new_entries.items()):
        dictionary[key] = construct_pdf_object(value, pdf, contents,
                                               compression_allowed = compression_allowed,
                                               encryption_allowed = encryption_allowed)

def construct_pdf_object(pdf_object_expression, pdf, contents,
                         compression_allowed = True,
                         encryption_allowed = True,
                         object_no = None):

    (tag, value) = pdf_object_expression
    if tag == 'PDF_Null':
        result = pdfobj.pdf_null(value)
    elif tag == 'PDF_Name':
        result = pdfobj.pdf_name(value)
    elif tag == 'PDF_String':
        result = pdfobj.pdf_string(pdfobj.encode_user_readably(value),
                                   encryption_allowed = encryption_allowed)
    elif tag == 'PDF_Bool':
        result = pdfobj.pdf_bool(value)
    elif tag == 'PDF_Int':
        result = pdfobj.pdf_int(value)
    elif tag == 'PDF_Real':
        result = pdfobj.pdf_numeric(value)

    elif tag == 'PDF_Array':
        a = [construct_pdf_object(x, pdf, contents,
                                  compression_allowed = compression_allowed,
                                  encryption_allowed = encryption_allowed) for x in value]
        result = pdfobj.pdf_array(a)

    elif tag == 'PDF_Dict':
        d = {}
        update_pdf_dictionary(d, pdf, contents, value,
                              compression_allowed = compression_allowed,
                              encryption_allowed = encryption_allowed)
        result = pdfobj.pdf_dict(d)

    elif tag == 'PDF_Indirect':
        subvalue = construct_pdf_object(value, pdf, contents,
                                        object_no = None,
                                        compression_allowed = compression_allowed,
                                        encryption_allowed = encryption_allowed)
        indirect_obj = pdfobj.indirect(subvalue,  identity_function, object_no = object_no)
        if object_no != None:
            result = subvalue
        else:
            result = pdfobj.pdf_reference(indirect_obj)

    elif tag == 'PDF_Unencrypted':
        result = construct_pdf_object(value, pdf, contents,
                                      compression_allowed = compression_allowed,
                                      encryption_allowed = False,
                                      object_no = object_no)

    elif tag == 'PDF_Uncompressed':
        result = construct_pdf_object(value, pdf, contents,
                                      compression_allowed = False,
                                      encryption_allowed = encryption_allowed,
                                      object_no = object_no)

    elif tag == 'PDF_ByteStream':
        ((tag1, dictionary), data) = value
        if tag1 != 'PDF_Dict':
            raise PDFObjectError('ByteStream expects a PDF_Dict, but got a ' + tag1)
        d = {}
        update_pdf_dictionary(d, pdf, contents, dictionary,
                              compression_allowed = compression_allowed,
                              encryption_allowed = encryption_allowed)
        obj = embed_data_as_stream(pdf, data, stream_dict = d,
                                   object_no = object_no,
                                   compression_allowed = compression_allowed,
                                   encryption_allowed = encryption_allowed)
        result = pdfobj.pdf_reference(obj)

    elif tag == 'PDF_FileStream':
        ((tag1, dictionary), file_name) = value
        if tag1 != 'PDF_Dict':
            raise PDFObjectError('FileStream expects a PDF_Dict, but got a ' + tag1)
        d = {}
        update_pdf_dictionary(d, pdf, contents, dictionary,
                              compression_allowed = compression_allowed,
                              encryption_allowed = encryption_allowed)
        obj = embed_file_as_stream(pdf, file_name, stream_dict = d,
                                   object_no = object_no,
                                   compression_allowed = compression_allowed,
                                   encryption_allowed = encryption_allowed)
        result = pdfobj.pdf_reference(obj)

    elif tag == 'PDF_ImageStream':
        obj = embed_image_as_stream(pdf, contents, value,
                                    object_no = object_no,
                                    compression_allowed = compression_allowed,
                                    encryption_allowed = encryption_allowed)
        result = pdfobj.pdf_reference(obj)

    elif tag == 'PDF_Reference':
        obj = pdfobj.object_list[value - 1]
        generation = obj['generation']
        result = '{0} {1} R'.format(value, generation)

    else:
        raise PDFObjectError('Unrecognized tag: ' + tag)

    return result

#--------------------------------------------------------------------------

def build_page(pdf, fonts, xobjects, page_box, page_width, page_height):

    contents = { 'data'          : [],
                 'resources'     : {},
                 'fonts'         : fonts,
                 'xobjects'      : xobjects }
    
    content_data = streamify_box(pdf, contents, x = 0, y = page_height, box = page_box)

    contents['starting_offset'] = pdf.offset
    stream_dict = { }
    content_data_obj = embed_data_as_stream(pdf, content_data, stream_dict = stream_dict)
    contents['length'] = pdf.offset - contents['starting_offset']
    contents['data'].append(pdfobj.pdf_reference(content_data_obj))
    contents['contents_obj'] = content_data_obj

    for r in contents['resources']:
        contents['resources'][r] = pdfobj.pdf_dict(contents['resources'][r])

    proc_set = [pdfobj.pdf_name('PDF'), pdfobj.pdf_name('Text'), pdfobj.pdf_name('ImageB'),
                pdfobj.pdf_name('ImageC'), pdfobj.pdf_name('ImageI')]
    contents['resources']['ProcSet'] = pdfobj.pdf_array(proc_set)

    resources_indirect = pdfobj.indirect(contents['resources'], pdfobj.pdf_dict)

    page = { 'Type'      : pdfobj.pdf_name('Page'),
             'MediaBox'  : pdfobj.pdf_array([pdfobj.pdf_numeric(0), pdfobj.pdf_numeric(0),
                                      pdfobj.pdf_numeric(pt_to_bp(page_width)),
                                      pdfobj.pdf_numeric(pt_to_bp(page_height))]),
             'Resources' : pdfobj.pdf_reference(resources_indirect),
             'Contents'  : pdfobj.pdf_array(contents['data']) }
    return page

def streamify_box(pdf, contents, x, y, box):
    result = b''
    if box[0] == 'Empty':
        pass
    elif box[0] == 'Rule':
        (ignored_, (w, h)) = box
        result = streamify_rule(x, y, w, h)
    elif box[0] == 'SimpleGlyph':
        simple_glyphs = [('0', '0', box)]
        result = streamify_simple_glyphs(contents, x = x, y = y, simple_glyphs = simple_glyphs)
    elif box[0] == 'Image':
        (ignored_, (w, h, f)) = box
        result = streamify_image(pdf, contents, x, y, w, h, f)
    elif box[0] == 'Group':
        (ignored_, gfx_cmds) = box
        result = streamify_group(pdf, contents, x, y, gfx_cmds)
    elif box[0] == 'Command':
        (ignored_, command) = box
        if command[0] == 'PDF_Code':
            result = b'\n' + command[1] + b'\n'
        elif command[0] == 'PDF_Info':
            pdf.update_dictionary(contents, 'Info', command[1])
        elif command[0] == 'PDF_Catalog':
            pdf.update_dictionary(contents, 'Catalog', command[1])
        elif command[0] == 'PDF_Register':
            (obj, obj_no) = command[1]
            pdf.register_object(contents, obj, obj_no)
    return result

def streamify_rule(x, y, w, h):
    return (pdfobj.pdf_numeric(pt_to_bp(x)) + b' ' +
            pdfobj.pdf_numeric(pt_to_bp(y)) + b' ' +
            pdfobj.pdf_numeric(pt_to_bp(w)) + b' ' +
            pdfobj.pdf_numeric(pt_to_bp(h)) + b' re f ')

def streamify_image(pdf, contents, x, y, w, h, file_name):

    if 'XObject' not in contents['resources']:
        contents['resources']['XObject'] = {}
    xobjects_dict = contents['resources']['XObject']

    xobjects = contents['xobjects']

    if file_name in xobjects:
        image = xobjects[file_name]
    else:
        t = file_type.file_type(file_name)
        if t == 'pdf':
            obj = register_pdf(pdf, contents, w, h, file_name)
        elif t == 'svg':
            pdf_file_name = os.path.join(temp_dir.temp_dir(), os.path.basename(file_name) + '.pdf')
            p = subprocess.Popen(['svg2pdf', file_name, pdf_file_name])
            os.waitpid(p.pid, 0)
            obj = register_pdf(pdf, contents, w, h, pdf_file_name, registered_name = file_name)
        elif t == 'postscript':
            pdf_file_name = os.path.join(temp_dir.temp_dir(), os.path.basename(file_name) + '.pdf')
            p = subprocess.Popen(['ps2pdf14', file_name, pdf_file_name])
            os.waitpid(p.pid, 0)
            obj = register_pdf(pdf, contents, w, h, pdf_file_name, registered_name = file_name)
        elif t == 'wmf':
            svg_file_name = os.path.join(temp_dir.temp_dir(), os.path.basename(file_name) + '.svg')
            pdf_file_name = os.path.join(temp_dir.temp_dir(), os.path.basename(file_name) + '.pdf')
            p = subprocess.Popen(['wmf2svg', '-o', svg_file_name, file_name])
            os.waitpid(p.pid, 0)
            p = subprocess.Popen(['svg2pdf', svg_file_name, pdf_file_name])
            os.waitpid(p.pid, 0)
            obj = register_pdf(pdf, contents, w, h, pdf_file_name, registered_name = file_name)
        else:
            obj = register_image(pdf, xobjects, file_name)

        image = xobjects[file_name]
        xobjects[obj['object_no']] = image # Allow indexing by object number.

    image_name = image['internal_xobject_name']
    if image_name not in xobjects_dict:
        xobjects_dict[image_name] = image['xobject_stream']

    return (b'q ' + pdfobj.pdf_numeric(pt_to_bp(w)) + b' 0 0 ' +
            pdfobj.pdf_numeric(pt_to_bp(h)) + b' ' +
            pdfobj.pdf_numeric(pt_to_bp(x)) + b' ' +
            pdfobj.pdf_numeric(pt_to_bp(y)) + b' cm\n' +
            pdfobj.pdf_name(image_name) + b' Do Q\n')

def streamify_group(pdf, contents, x, y, gfx_cmds):
    result = b'q '
    i = 0
    while i < len(gfx_cmds):

        if gfx_cmds[i][0] == 'PutBox':
            (ignored_, (dx, dy, box)) = gfx_cmds[i]
            if box[0] == 'SimpleGlyph':
                simple_glyphs = [gfx_cmds[i][1]]
                j = i + 1
                while (j < len(gfx_cmds) and
                       gfx_cmds[j][0] == 'PutBox' and
                       gfx_cmds[j][1][2][0] == 'SimpleGlyph'):
                    simple_glyphs.append(gfx_cmds[j][1])
                    j = j + 1
                result += streamify_simple_glyphs(contents, x = x, y = y, simple_glyphs = simple_glyphs)
                i = j
            else:
                result += streamify_box(pdf, contents, x = x + dx, y = y + dy, box = box)
                i = i + 1

        elif gfx_cmds[i][0] == 'SetColour':
            result += set_color(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'SetBgColour':
            result += set_bg_color(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'SetAlpha':
            result += set_alpha(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'SetLineWidth':
            result += set_line_width(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'SetMiterLimit':
            result += set_miter_limit(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'SetLineCap':
            result += set_line_cap(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'SetLineJoin':
            result += set_line_join(gfx_cmds[i][1])
            i = i + 1

        elif gfx_cmds[i][0] == 'Draw':
            (path_cmd, path) = gfx_cmds[i][1]
            result += draw_path(x, y, path_cmd, path)
            i = i + 1

        else:
            print('****** Unrecognized graphics command: ' + gfx_cmds[i][0] + ' ******')
            i = i + 1

    result += b'Q\n'
    return result

def set_color(color):
    if color[0] == 'Gray':
        (ignored_, n) = color
        result = pdfobj.pdf_numeric(n) + b' g\n'
    elif color[0] == 'RGB':
        (ignored_, r, g, b) = color
        result = (pdfobj.pdf_numeric(r) + b' ' +
                  pdfobj.pdf_numeric(g) + b' ' +
                  pdfobj.pdf_numeric(b) + b' rg\n')
    elif color[0] == 'CMYK':
        (ignored_, c, m, y, k) = color
        result = (pdfobj.pdf_numeric(c) + b' ' +
                  pdfobj.pdf_numeric(m) + b' ' +
                  pdfobj.pdf_numeric(y) + b' ' +
                  pdfobj.pdf_numeric(k) + b' k\n')
    return result

def set_bg_color(color):
    return b''       # FIX: NOT YET IMPLEMENTED and probably won’t be.

def set_alpha(alpha):
    return b''                  # FIX: NOT YET IMPLEMENTED.

def set_line_width(width):
    return pdfobj.pdf_numeric(pt_to_bp(width)) + b' w\n'

def set_miter_limit(limit):
    return pdfobj.pdf_numeric(limit) + b' M\n'

def set_line_cap(pdf, cap):
    return { 'Butt'   : b'0 J\n',
             'Circle' : b'1 J\n',
             'Square' : b'2 J\n' }[cap]

def set_line_join(pdf, join):
    return { 'Miter' : b'0 j\n',
             'Round' : b'1 j\n',
             'Bevel' : b'2 j\n' }[join]

def draw_path(x, y, path_cmd, path):
    path_op = { 'Stroke' : b'S',
                'Fill'   : b'f ',
                'Clip'   : b'W n' }[path_cmd]
    result = b''
    if path != []:
        (cur_x, cur_y, ignored_, ignored_, ignored_, ignored_, ignored_, ignored_) = path[0]
        result += (pdfobj.pdf_numeric(pt_to_bp (x + cur_x)) + b' ' +
                   pdfobj.pdf_numeric(pt_to_bp (y + cur_y)) + b' m ')
        for (a_x, a_y, b_x, b_y, c_x, c_y, d_x, d_y) in path:
            if a_x != cur_x or a_y != cur_y:
                result += (path_op + b' ' +
                           pdfobj.pdf_numeric(pt_to_bp(x + cur_x)) + b' ' +
                           pdfobj.pdf_numeric(pt_to_bp(y + cur_y)) + b' m ')
            result += (pdfobj.pdf_numeric(pt_to_bp(x + b_x)) + b' ' +
                       pdfobj.pdf_numeric(pt_to_bp(y + b_y)) + b' ' +
                       pdfobj.pdf_numeric(pt_to_bp(x + c_x)) + b' ' +
                       pdfobj.pdf_numeric(pt_to_bp(y + c_y)) + b' ' +
                       pdfobj.pdf_numeric(pt_to_bp(x + d_x)) + b' ' +
                       pdfobj.pdf_numeric(pt_to_bp(y + d_y)) + b' c ')
            (cur_x, cur_y) = (d_x, d_y)
        result += path_op + b'\n'
    return result

def streamify_simple_glyphs(contents, x, y, simple_glyphs):

    if 'Font' not in contents['resources']:
        contents['resources']['Font'] = {}
    fonts_dict = contents['resources']['Font']

    fonts = contents['fonts']

    single_font_glyphs = pdfobj.split_by_font(simple_glyphs)

    result = b'BT '

    for (fm, glyphs) in single_font_glyphs:

        if fm['file_name'] not in fonts:
            register_font(fonts, fm)

        if fm['font_type'] == 'TFM':
            code_to_glyph_index = fm['code_to_glyph_index']
            character_size = 1
        else:
            code_to_glyph_index = None
            character_size = 2

        font = fonts[fm['file_name']]
        glyphs_used = font['glyphs_used']
        font_name = font['internal_font_name']
        if font_name not in fonts_dict:
            fonts_dict[font_name] = font['font_dict_reference']
        point_size = pt_to_bp(fm['at_size'])

        result += (pdfobj.pdf_numeric(point_size) + b' 0 0 ' + pdfobj.pdf_numeric(point_size) + b' ' +
                   pdfobj.pdf_numeric(pt_to_bp(x)) + b' ' + pdfobj.pdf_numeric(pt_to_bp(y)) + b' Tm\n' +
                   pdfobj.pdf_name(font_name) + b' 1 Tf\n')

        prepped_glyphs = pdfobj.prepare_glyphs(glyphs, fonts, fm)
        partitioned_glyphs = pdfobj.partition_glyphs(prepped_glyphs)
        result += pdfobj.streamify_partitioned_glyphs(partitioned_glyphs, point_size, glyphs_used)

    result += b'ET '
    return result

#--------------------------------------------------------------------------

def register_font(fonts, fm):
    assert(fm['file_name'] not in fonts)

    file_name = fm['file_name']
    font_name = 'F' + str(len(fonts) + 1)
    f = fm['ff_font']

    f.encoding = 'Original'
    font_info = get_font_info(f)
    font_info['advance_widths'] = get_advance_widths(fm)

    font_dict = {}
    font_dict_indirect = pdfobj.indirect(font_dict, pdfobj.pdf_dict)
    font_dict_reference = pdfobj.pdf_reference(font_dict_indirect)

    fonts[file_name] = {
        'font_dict_reference' : font_dict_reference,
        'font_dict'           : font_dict,
        'font_metric'         : fm,
        'file_name'           : file_name,
        'internal_font_name'  : font_name,
        'glyphs_used'         : set([0]),
        'font_info'           : font_info,
        }

def get_font_info(f):

    font_info = {}
    font_info['font_name']    = f.fontname
    font_info['italic_angle'] = f.italicangle

    font_info['em']           = f.em

    # FIX: Are these the right values to use?
    font_info['ascent']       = f.ascent
    font_info['descent']      = -f.descent

    if 0 <= f.capHeight:
        font_info['cap_height'] = f.capHeight
    if 0 <= f.xHeight:
        font_info['ex_height']  = f.xHeight

    if 'StdVW' in f.private:
        font_info['stem_v'] = float(f.private['StdVW'][0])
    if 'StdHW' in f.private:
        font_info['stem_h'] = float(f.private['StdHW'][0])

    return font_info

def get_advance_widths(fm):
    f = fm['ff_font']
    advance_widths = []
    for i in range(len(f)):
        advance_widths.append(f[i].width)
    return advance_widths

#--------------------------------------------------------------------------

def write_font(pdf, font):
    write_font_as_cid_cff(pdf, font)

#--------------------------------------------------------------------------

def write_font_as_cid_cff(pdf, font):

    fm = font['font_metric']
    font_dict = font['font_dict']
    glyphs_used = font['glyphs_used']
    internal_font_name = font['internal_font_name']
    font_info = font['font_info']

    cid_system_info = { 'Registry'   : pdfobj.pdf_string(b'Adobe'),
                        'Ordering'   : pdfobj.pdf_string(b'Identity'),
                        'Supplement' : pdfobj.pdf_numeric(0) }

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    font_stream_dict = { 'Subtype' : pdfobj.pdf_name('CIDFontType0C') }

    cid_cff_file_name = os.path.join(temp_dir.temp_dir(), internal_font_name + '.cid.cff')
    generate_cid_cff(pdf, font, cid_cff_file_name)

    with open(cid_cff_file_name, 'rb') as font_file:
        data = mmap.mmap(font_file.fileno(), 0, prot = mmap.PROT_READ)
        rx = re.compile(br'.* ([0-9]+) StartData[\r\n](.*)', re.DOTALL)
        m = rx.match(data)
        cff_size = int(m.group(1))
        remaining_bytes = cff_size - len(m.group(2))
        font_stream = embed_data_as_stream(pdf, m.group(2)[:remaining_bytes],
                                           stream_dict = font_stream_dict)
        data.close()

    if 'subset_tag' in font_info and font_info['subset_tag'] != None:
        tag_prefix = font_info['subset_tag'] + '+'
    else:
        tag_prefix = ''

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    font_descriptor = { 'Type' : pdfobj.pdf_name('FontDescriptor') }

    if 'FontName' in font_info:
        font_descriptor['FontName'] = pdfobj.pdf_name(tag_prefix + font_info['FontName'])
    else:
        font_descriptor['FontName'] = pdfobj.pdf_name(tag_prefix + font_info['font_name'])

    font_descriptor['Flags'] = pdfobj.pdf_numeric(6) # FIX: Set this to more correct values.

    scale = 1000.0 / font_info['em']

    font_descriptor['FontBBox'] = pdfobj.pdf_array([pdfobj.pdf_numeric(b * scale) for b in font_info['font_bounding_box']])

    font_descriptor['ItalicAngle'] = pdfobj.pdf_numeric(font_info['italic_angle'])

    font_descriptor['Ascent'] = pdfobj.pdf_numeric(font_info['ascent'] * scale)
    font_descriptor['Descent'] = pdfobj.pdf_numeric(font_info['descent'] * scale)

    try: font_descriptor['CapHeight'] = pdfobj.pdf_numeric(font_info['cap_height'] * scale)
    except: font_descriptor['CapHeight'] = pdfobj.pdf_numeric(0)

    try: font_descriptor['XHeight'] = pdfobj.pdf_numeric(font_info['ex_height'] * scale)
    except: pass

    try: font_descriptor['StemV'] = pdfobj.pdf_numeric(font_info['stem_v'] * scale)
    except: font_descriptor['StemV'] = pdfobj.pdf_numeric(0) # FIX: Put something better here, such as fontforge's best guess.

    try: font_descriptor['StemH'] = pdfobj.pdf_numeric(font_info['stem_h'] * scale)
    except: pass

    font_descriptor['FontFile3'] = pdfobj.pdf_reference(font_stream)

    font_descriptor_indirect = pdfobj.indirect(font_descriptor, pdfobj.pdf_dict)

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    font_dict1 = { 'Type'     : pdfobj.pdf_name('Font'),
                   'Subtype'  : pdfobj.pdf_name('CIDFontType0'),
                   'BaseFont' : font_descriptor['FontName'],
                   'FontDescriptor' : pdfobj.pdf_reference(font_descriptor_indirect),
                   'CIDSystemInfo'  : pdfobj.pdf_dict(cid_system_info),
                   'W'        : pdfobj.pdf_array(glyph_widths(font_info['advance_widths'], glyphs_used)) }

    font_dict1_indirect = pdfobj.indirect(font_dict1, pdfobj.pdf_dict)

    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    unicode_values = get_unicode_values(fm, glyphs_used)
    tounicode = create_tounicode(unicode_values)
    tounicode_stream = embed_data_as_stream(pdf, tounicode.encode('ASCII'))

    font_dict['Type'] = pdfobj.pdf_name('Font')
    font_dict['Subtype'] = pdfobj.pdf_name('Type0')
    font_dict['DescendantFonts'] = pdfobj.pdf_array([pdfobj.pdf_reference(font_dict1_indirect)])
    font_dict['BaseFont'] = font_descriptor['FontName']
    font_dict['ToUnicode'] = pdfobj.pdf_reference(tounicode_stream)
    font_dict['Encoding'] = pdfobj.pdf_name('Identity-H')

#---------------------------------------------------------------------------

pdf_colorspace = {
    image.image_type.GRAYSCALE        : 'DeviceGray',
    image.image_type.TRUECOLOR        : 'DeviceRGB',
    image.image_type.COLOR_SEPARATION : 'DeviceCMYK',

    image.image_type.GRAYSCALE_MATTE        : 'DeviceGray',
    image.image_type.TRUECOLOR_MATTE        : 'DeviceRGB',
    image.image_type.COLOR_SEPARATION_MATTE : 'DeviceCMYK',
    }

pixel_order = {
    image.image_type.GRAYSCALE        : 'I',
    image.image_type.TRUECOLOR        : 'RGB',
    image.image_type.COLOR_SEPARATION : 'CMYK',

    image.image_type.GRAYSCALE_MATTE        : 'I',
    image.image_type.TRUECOLOR_MATTE        : 'RGB',
    image.image_type.COLOR_SEPARATION_MATTE : 'CMYK',
    }

preferred_image_type = {
    image.image_type.BILEVEL          : image.image_type.GRAYSCALE,
    image.image_type.GRAYSCALE        : image.image_type.GRAYSCALE,
    image.image_type.PALETTE          : image.image_type.TRUECOLOR, # FIX: Needs testing; TODO: handle palettes?
    image.image_type.TRUECOLOR        : image.image_type.TRUECOLOR,
    image.image_type.COLOR_SEPARATION : image.image_type.COLOR_SEPARATION,

    image.image_type.GRAYSCALE_MATTE        : image.image_type.GRAYSCALE_MATTE,
    image.image_type.PALETTE_MATTE          : image.image_type.TRUECOLOR_MATTE, # FIX: Needs testing; TODO: handle palettes?
    image.image_type.TRUECOLOR_MATTE        : image.image_type.TRUECOLOR_MATTE,
    image.image_type.COLOR_SEPARATION_MATTE : image.image_type.COLOR_SEPARATION_MATTE,
}

def register_image(pdf, xobjects, file_name,
                   object_no = None,
                   compression_allowed = True,
                   encryption_allowed = True):

    assert(file_name not in xobjects)

    # FIX/TODO: Support for indexed color.

    # Possible TODO: Use PNG predictors, per row, to get possibly
    # better compression than we get from TIFF predictor 2.

    internal_xobject_name = 'X' + str(len(xobjects) + 1)

    xobject_stream_dict = { 'Type'    : pdfobj.pdf_name('XObject'),
                            'Subtype' : pdfobj.pdf_name('Image') }

    w = image.magick_wand()
    w.read_image(file_name)

    format = w.get_format()
    width  = w.get_width()
    height = w.get_height()

    xobject_stream_dict['Width'] = pdfobj.pdf_int(width)
    xobject_stream_dict['Height'] = pdfobj.pdf_int(height)

    if format == 'JP2':

        # JPEG2000 ----------------------------------------------------------------------------

        xobject_stream_dict['Filter'] = pdfobj.pdf_name('JPXDecode')
        #if has_alpha:   # FIX: The author does not know how to code in-data transparency support for JPEG2000.
        #    xobject_stream_dict['SMaskInData'] = pdfobj.pdf_int(1)
        xobject_stream = embed_file_as_stream(pdf, file_name,
                                              stream_dict = xobject_stream_dict,
                                              object_no = object_no,
                                              compression_allowed = False, # The image already is compressed.
                                              encryption_allowed = encryption_allowed)
        # -------------------------------------------------------------------------------------

    else:

        img_type = preferred_image_type[w.get_image_type()]
        w.set_image_type(img_type)

        bit_depth = 8           # FIX: Handle bit-depths other than 8.

        xobject_stream_dict['ColorSpace'] = pdfobj.pdf_name(pdf_colorspace[img_type])
        xobject_stream_dict['BitsPerComponent'] = pdfobj.pdf_int(bit_depth)

        if format == 'JPEG':

            # JPEG ----------------------------------------------------------------------------

            xobject_stream_dict['Filter'] = pdfobj.pdf_name('DCTDecode')
            xobject_stream = embed_file_as_stream(pdf, file_name,
                                                  stream_dict = xobject_stream_dict,
                                                  object_no = object_no,
                                                  compression_allowed = False, # The image already is compressed.
                                                  encryption_allowed = encryption_allowed)
            # ---------------------------------------------------------------------------------

        else:


            order = pixel_order[img_type]
            storage = image.storage_type.CHAR_PIXEL # FIX: Support other bit-depths.

            alpha_channel = w.get_alpha_channel()
            has_alpha = (alpha_channel != image.boolean_type.FALSE)

            if not has_alpha:

                if compression_params.level:

                    # Compressed, no alpha layer ---------------------------------------------------------
                    decode_parameters = { 'Predictor' : pdfobj.pdf_int(2), # TIFF predictor 2.
                                          'Colors'    : pdfobj.pdf_int(len(order)),
                                          'Columns'   : pdfobj.pdf_int(width) }
                    xobject_stream_dict['DecodeParms'] = pdfobj.pdf_dict(decode_parameters)

                    data = w.export_pixels(0, 0, width, height, order, storage)
                    image.apply_tiff_predictor_2(width, height, order, storage, data)
                    xobject_stream = embed_data_as_stream(pdf, data, stream_dict = xobject_stream_dict,
                                                          object_no = object_no,
                                                          compression_allowed = compression_allowed,
                                                          encryption_allowed = encryption_allowed)
                    #-------------------------------------------------------------------------------------

                else:

                    # Uncompressed, no alpha layer -------------------------------------------------------
                    data = w.export_pixels(0, 0, width, height, order, storage)
                    xobject_stream = embed_data_as_stream(pdf, data, stream_dict = xobject_stream_dict,
                                                          object_no = object_no,
                                                          compression_allowed = compression_allowed,
                                                          encryption_allowed = encryption_allowed)
                    #-------------------------------------------------------------------------------------

            else:

                if compression_params.level:

                    # Compressed, with an alpha layer -----------------------------------------------

                    decode_parameters = { 'Predictor' : pdfobj.pdf_int(2), # TIFF predictor 2.
                                          'Colors'    : pdfobj.pdf_int(1),
                                          'Columns'   : pdfobj.pdf_int(width) }
                    mask_stream_dict = { 'Type'       : pdfobj.pdf_name('XObject'),
                                         'Subtype'    : pdfobj.pdf_name('Image'),
                                         'Width'      : pdfobj.pdf_int(width),
                                         'Height'     : pdfobj.pdf_int(height),
                                         'ColorSpace' : pdfobj.pdf_name('DeviceGray'),
                                         'BitsPerComponent' : pdfobj.pdf_int(bit_depth),
                                         'DecodeParms' : pdfobj.pdf_dict(decode_parameters) }
                    xobject_stream_dict['DecodeParms'] = pdfobj.pdf_dict(decode_parameters)


                    data = w.export_pixels(0, 0, width, height, 'A', storage)
                    image.apply_tiff_predictor_2(width, height, 'A', storage, data)
                    mask_stream = embed_data_as_stream(pdf, data, stream_dict = mask_stream_dict,
                                                       object_no = object_no,
                                                       compression_allowed = compression_allowed,
                                                       encryption_allowed = encryption_allowed)

                    decode_parameters = { 'Predictor' : pdfobj.pdf_int(2), # TIFF predictor 2.
                                          'Colors'    : pdfobj.pdf_int(len(order)),
                                          'Columns'   : pdfobj.pdf_int(width) }
                    xobject_stream_dict['DecodeParms'] = pdfobj.pdf_dict(decode_parameters)
                    xobject_stream_dict['SMask'] = pdfobj.pdf_reference(mask_stream)

                    data = w.export_pixels(0, 0, width, height, order, storage)
                    image.apply_tiff_predictor_2(width, height, order, storage, data)
                    xobject_stream = embed_data_as_stream(pdf, data, stream_dict = xobject_stream_dict,
                                                          object_no = object_no,
                                                          compression_allowed = compression_allowed,
                                                          encryption_allowed = encryption_allowed)

                    #-------------------------------------------------------------------------------------

                else:

                    # Uncompressed, with an alpha layer --------------------------------------------------

                    mask_stream_dict = { 'Type'       : pdfobj.pdf_name('XObject'),
                                         'Subtype'    : pdfobj.pdf_name('Image'),
                                         'Width'      : pdfobj.pdf_int(width),
                                         'Height'     : pdfobj.pdf_int(height),
                                         'ColorSpace' : pdfobj.pdf_name('DeviceGray'),
                                         'BitsPerComponent' : pdfobj.pdf_int(bit_depth) }

                    data = w.export_pixels(0, 0, width, height, 'A', storage)
                    mask_stream = embed_data_as_stream(pdf, data, stream_dict = mask_stream_dict,
                                                       object_no = object_no,
                                                       compression_allowed = compression_allowed,
                                                       encryption_allowed = encryption_allowed)

                    xobject_stream_dict['SMask'] = pdfobj.pdf_reference(mask_stream)

                    data = w.export_pixels(0, 0, width, height, order, storage)
                    xobject_stream = embed_data_as_stream(pdf, data, stream_dict = xobject_stream_dict,
                                                          object_no = object_no,
                                                          compression_allowed = compression_allowed,
                                                          encryption_allowed = encryption_allowed)

                    #-------------------------------------------------------------------------------------

    xobjects[file_name] = { 'xobject_stream'        : pdfobj.pdf_reference(xobject_stream),
                            'internal_xobject_name' : internal_xobject_name }
    return xobject_stream

#--------------------------------------------------------------------------

def embed_image_as_stream(pdf, contents, file_name,
                          object_no = None,
                          compression_allowed = True,
                          encryption_allowed = True):

    if 'XObject' not in contents['resources']:
        contents['resources']['XObject'] = {}
    xobjects_dict = contents['resources']['XObject']

    xobjects = contents['xobjects']

    if file_name not in xobjects:
        obj = register_image(pdf, xobjects, file_name,
                             object_no = object_no,
                             compression_allowed = compression_allowed,
                             encryption_allowed = encryption_allowed)

    image = xobjects[file_name]
    image_name = image['internal_xobject_name']
    if image_name not in xobjects_dict:
        xobjects_dict[image_name] = image['xobject_stream']

    return obj

#--------------------------------------------------------------------------

def register_pdf(pdf, contents, w, h, file_name, page_no = 0,
                 registered_name = None,
                 object_no = None):

    assert file_name not in contents['xobjects']
    internal_xobject_name = 'X' + str(len(contents['xobjects']) + 1)

    with open(file_name, 'rb') as f:
        reader = pyPdf.PdfFileReader(f)
        pg = reader.getPage(page_no)
        (x0, y0, x1, y1) = pg.mediaBox
        width = x1 - x0
        height = y1 - y0
        matrix = (decimal.Decimal(1)/width,
                  0, 0, decimal.Decimal(1)/height,
                  -x0/width, -y0/height)

        xobject_stream_dict = { 'Type'      : pdfobj.pdf_name('XObject'),
                                'Subtype'   : pdfobj.pdf_name('Form'),
                                'BBox'      : pdfobj.pdf_array([pdfobj.pdf_numeric(t) for t in pg.mediaBox]),
                                'Matrix'    : pdfobj.pdf_array([pdfobj.pdf_numeric(t, precision = 10) for t in matrix]),
                                'Resources' : transfer_resources(pdf, contents, pg) }

        data = b''
        if hasattr(pg['/Contents'], 'getData'):
            data += pg['/Contents'].getData()
        else:
            for c in pg['/Contents']:
                data += c.getObject().getData() + b'\n'

        xobject_stream = embed_data_as_stream(pdf, data,
                                              stream_dict = xobject_stream_dict,
                                              object_no = object_no)

    if registered_name == None:
        registered_name = file_name
    contents['xobjects'][registered_name] = { 'xobject_stream'        : pdfobj.pdf_reference(xobject_stream),
                                              'internal_xobject_name' : internal_xobject_name }
    return xobject_stream

def transfer(r, pdf, contents, pg, indirect_objects):

    if isinstance(r, pyPdf.generic.NullObject):
        result = pdfobj.pdf_null(None)

    elif isinstance(r, pyPdf.generic.NameObject):
        result = pdfobj.pdf_name(str(r)[1:])

    elif (isinstance(r, pyPdf.generic.TextStringObject) or
          isinstance(r, pyPdf.generic.ByteStringObject)):
        result = pdfobj.pdf_string(r.original_bytes)

    elif (isinstance(r, pyPdf.generic.NumberObject) or
          isinstance(r, pyPdf.generic.FloatObject)):
        result = pdfobj.pdf_numeric(r)

    elif isinstance(r, pyPdf.generic.BooleanObject):
        result = pdfobj.pdf_bool(bool(r))

    elif isinstance(r, pyPdf.generic.StreamObject):
        d = {}
        for res in r:
            if res != '/Length' and res != '/Contents':
                d[res[1:]] = transfer(r[res], pdf, contents, pg, indirect_objects)
        data = r._data
        indirect_obj = embed_data_as_stream(pdf, data, stream_dict = d)
        result = pdfobj.pdf_reference(indirect_obj)

    elif isinstance(r, pyPdf.generic.DictionaryObject):
        d = {}
        for res in r:
            d[res[1:]] = transfer(r[res], pdf, contents, pg, indirect_objects)
        result = pdfobj.pdf_dict(d)

    elif isinstance(r, pyPdf.generic.ArrayObject):
        result = pdfobj.pdf_array([transfer(res, pdf, contents, pg, indirect_objects) for res in r])

    elif isinstance(r, pyPdf.generic.IndirectObject):
        s = repr(r)
        try:
            result = indirect_objects[s]
        except:
            obj = transfer(r.getObject(), pdf, contents, pg, indirect_objects)
            indirect_obj = pdfobj.indirect(obj, identity_function)
            result = pdfobj.pdf_reference(indirect_obj)
            indirect_objects[s] = result

    else:
        print('****** Unexpected or unrecognized pyPdf type: ' + str(type(r)) + ' ******')
        result = None

    return result

def transfer_resources(pdf, contents, pg):
    indirect_objects = {}
    return transfer(pg['/Resources'], pdf, contents, pg, indirect_objects)

#--------------------------------------------------------------------------

def glyph_widths(advance_widths, glyph_subset):
    if glyph_subset == None:
        widths = [0, pdfobj.pdf_array([pdfobj.pdf_numeric(w) for w in advance_widths])]
    else:
        partitions = [0, [advance_widths[0]]]
        i = 1
        while i < len(advance_widths):
            while i < len(advance_widths) and i in glyph_subset:
                partitions[-1].append(advance_widths[i])
                i = i + 1
            while i < len(advance_widths) and i not in glyph_subset:
                i = i + 1
            if i < len(advance_widths):
                partitions.append(i)
                partitions.append([advance_widths[i]])
                i = i + 1
        widths = []
        for p in partitions:
            if type(p) == type(0):
                widths.append(pdfobj.pdf_int(p))
            else:
                widths.append(pdfobj.pdf_array([pdfobj.pdf_numeric(w) for w in p]))
    return widths

def generate_cid_cff(pdf, font, cid_cff_file_name):

    file_name = font['file_name']
    font_name = font['internal_font_name']
    glyphs_used = font['glyphs_used']
    font_info = font['font_info']
    font_metric = font['font_metric']

    f = font_metric['ff_font']

    cmap = kpathsea.find_resource('Identity-H', kpathsea.kpse_cmap_format)
    f.cidConvertByCmap(cmap)

    tag = make_font_subset_tag()        
    font_info['subset_tag'] = tag

    for g in f.glyphs():
        if g.originalgid not in glyphs_used:
            f.removeGlyph(g)

    # Measure the bounding box, using only glyphs included in the
    # subset. The ‘Select Tool’ in Adobe Reader uses this bounding
    # box information.
    #
    # In the next line, any really large values ought to suffice.
    bounding_box = (sys.maxsize, sys.maxsize, -sys.maxsize, -sys.maxsize)
    for g in f.glyphs():
        if g.originalgid in glyphs_used:
            glyph_bounding_box = g.boundingBox()
            bounding_box = (min(bounding_box[0], glyph_bounding_box[0]),
                            min(bounding_box[1], glyph_bounding_box[1]),
                            max(bounding_box[2], glyph_bounding_box[2]),
                            max(bounding_box[3], glyph_bounding_box[3]))
    font_info['font_bounding_box'] = bounding_box

    if font_params.no_hinting:
        for g in f.glyphs():
            g.hhints = ()
            g.vhints = ()
        hinting_flags = ['no-hints', 'no-flex']
    else:
        hinting_flags = []

    f.generate(cid_cff_file_name, flags = hinting_flags)

    return font_info

#--------------------------------------------------------------------------

assigned_subset_tags = set([''])

def make_font_subset_tag():
    tag = ''
    while tag in assigned_subset_tags:
        tag = ''
        for i in range(0, 6):
            tag = tag + chr(ord('A') + random.randint(0, 25))
    assigned_subset_tags.add(tag)
    return tag

#--------------------------------------------------------------------------

def pdf_header(version = '1.7', the_pdf_is_binary = True):
    s = b'%PDF-' + version.encode('UTF-8') + b'\n'
    if the_pdf_is_binary:
        s = s + b'%\342\343\317\323\n'
    return s

def trailer_dict(root, size, info = None, id_string = None, encrypt = None):
    d = { 'Root' : root,
          'Size' : pdfobj.pdf_int(size) }
    if info != None:
        d['Info'] = info
    if id_string != None:
        d['ID'] = pdfobj.pdf_array([pdfobj.pdf_hex_string(id_string, encryption_allowed = False),
                             pdfobj.pdf_hex_string(id_string, encryption_allowed = False)])
    if encrypt != None:
        d['Encrypt'] = pdfobj.pdf_dict(encrypt)
    return b'trailer\n' + pdfobj.pdf_dict(d)

def startxref(xref_offset):
    return b'\nstartxref\n' + pdfobj.pdf_int(xref_offset) + b'\n%%EOF\n'

def flush_objects(pdf):
    for i in range(0, len(pdfobj.object_list)):
        obj = pdfobj.object_list[i]
        if obj['offset'] == None and obj['string_maker'] != None:
            write_indirect_object(pdf, obj)

def write_indirect_object(pdf, obj):
    obj['offset'] = pdf.offset
    pdf.write_string(str(obj['object_no']) + ' ' + str(obj['generation']) + ' ' + 'obj\n')
    pdf.write(obj['string_maker'](obj['data']))
    pdf.write(b'\nendobj\n')

def write_xref_entry(pdf, position, generation, entry_type):
    s = '{0:010d} {1:05d} {2}\r\n'.format(position, generation, entry_type)
    assert len(s) == 20        
    pdf.write_string(s)

def next_free_xref_entry(obj_list, j):
    while j < len(obj_list) and obj_list[j]['offset'] != None:
        j += 1
    return (0 if j == len(obj_list) else j + 1)

def write_xref(pdf):

    obj_list = pdfobj.object_list

    pdf.write(b'xref\n')
    pdf.write_string(str(0) + ' ' + str(len(obj_list) + 1) + '\n')
    next_free = next_free_xref_entry(obj_list, 0)
    write_xref_entry(pdf, next_free, 65535, 'f')
    for i in range(0, len(obj_list)):
        obj = obj_list[i]
        if i + 1 == next_free:
            next_free = next_free_xref_entry(obj_list, i + 1)
            write_xref_entry(pdf, next_free, obj['generation'], 'f')
        else:
            write_xref_entry(pdf, obj['offset'], obj['generation'], 'n')

#--------------------------------------------------------------------------

def list_filters(dictionary, filter_array, key = 'Filter'):
    if len(filter_array) != 0:
        if len(filter_array) == 1:
            dictionary[key] = pdfobj.pdf_name(filter_array[0])
        else:
            dictionary[key] = pdfobj.pdf_array([pdfobj.pdf_name(f) for f in filter_array])

def embed_data_as_stream(pdf, data, stream_dict = {}, generation = 0,
                         object_no = None,
                         compression_allowed = True,
                         encryption_allowed = True):

    if object_no != None:
        obj = pdfobj.object_list[object_no - 1]
        obj['offset'] = pdf.offset
    else:
        object_no = len(pdfobj.object_list) + 1
        obj = { 'object_no'  : object_no,
                'generation' : generation,
                'offset'     : pdf.offset }
        pdfobj.object_list.append(obj)

    pdf.write_string(str(object_no) + ' ' + str(generation) + ' ' + 'obj\n')

    filters = []
    if compression_allowed and compression_params.level:
        filters = ['FlateDecode'] + filters
        compressed_data = zlib.compress(data)
    else:
        compressed_data = data
    list_filters(stream_dict, filters)

    if encryption_allowed and pdf_crypto.crypto_params.encryption_algorithm in (2, 4):
        encrypted_data = pdf_crypto.encrypt_object_data(object_no, generation, compressed_data)
    else:
        encrypted_data = compressed_data

    stream_dict['Length'] = pdfobj.pdf_numeric(len(encrypted_data))
    pdf.write(pdfobj.pdf_dict(stream_dict))
    pdf.write(b'\nstream\n')
    pdf.write(encrypted_data)
    pdf.write(b'\nendstream\n')
    pdf.write(b'endobj\n')
    return obj

def embed_file_as_stream(pdf, file_name, stream_dict = {}, generation = 0,
                         object_no = None,
                         compression_allowed = True,
                         encryption_allowed = True):
    with open(file_name, 'rb') as f:
        data = mmap.mmap(f.fileno(), 0, prot = mmap.PROT_READ)
        obj = embed_data_as_stream(pdf, data,
                                   stream_dict = stream_dict,
                                   generation = generation,
                                   object_no = object_no,
                                   compression_allowed = compression_allowed,
                                   encryption_allowed = encryption_allowed)
        data.close()
    return obj

#--------------------------------------------------------------------------

def get_unicode_values(fm, glyph_subset):
    first_glyph = fm['first_glyph']
    last_glyph = fm['last_glyph']
    unicodes = fm['unicodes']
    code_to_glyph_index = fm['code_to_glyph_index']

    unicode_values = []
    gids_already_handled = set()
    for i in range(last_glyph - first_glyph + 1):
        gid = code_to_glyph_index(first_glyph + i)
        if gid not in gids_already_handled and gid in glyph_subset:
            gids_already_handled.add(gid)
            u = unicodes[i].encode('UTF-16BE')
            if u != b'':
                h = ''
                for c in u:
                    h += '{0:02X}'.format(c)
                unicode_values.append(('{0:04X}'.format(gid), h))
    return unicode_values

def hex_plus_one(h):
    last_byte = int(h[-2:], 16)
    return ('invalid' if last_byte == 0xFF else h[:-2] + '{0:02X}'.format(last_byte + 1))

def gather_tounicode_ranges(unicode_values):

    groups = []
    i = 0
    while i < len(unicode_values):
        new_group = [unicode_values[i]]
        i = i + 1
        while (i < len(unicode_values) and 
               unicode_values[i][0] == hex_plus_one(new_group[-1][0]) and
               unicode_values[i][1] == hex_plus_one(new_group[-1][1])):
            new_group.append(unicode_values[i])
            i = i + 1
        groups.append(new_group)

    singles = []
    ranges = []
    for r in groups:
        if len(r) == 1:
            singles.append(r[0])
        else:
            (gid1, uni) = r[0]
            (gid2, ignored_)  = r[-1]
            ranges.append('<{0}><{1}><{2}>'.format(gid1, gid2, uni))

    groups = []
    i = 0
    while i < len(singles):
        new_group = [singles[i]]
        i = i + 1
        while (i < len(singles) and 
               singles[i][0] == hex_plus_one(new_group[-1][0])):
            new_group.append(singles[i])
            i = i + 1
        groups.append(new_group)    

    pure_singles = []
    pseudo_ranges = []
    for r in groups:
        if len(r) == 1:
            (gid, uni) = r[0]
            pure_singles.append('<{0}><{1}>'.format(gid, uni))
        else:
            (gid1, ignored_) = r[0]
            (gid2, ignored_) = r[-1]
            uni = ''
            for (ignored_, u) in r:
                uni = uni + '<{0}>'.format(u)
            pseudo_ranges.append('<{0}><{1}>[{2}]'.format(gid1, gid2, uni))

    ranges = ranges + pseudo_ranges
    ranges.sort()
    return (ranges, pure_singles)

def create_tounicode(unicode_values):

    cmap_name = 'Adobe-Identity-UCS'

    s = '/CIDInit /ProcSet findresource begin\n'
    s = s + '12 dict begin\n'
    s = s + 'begincmap\n'
    s = s + '/CMapName /' + cmap_name + ' def\n'
    s = s + '/CMapType 2 def\n'
    s = s + '/CIDSystemInfo <<\n'
    s = s + '  /Registry (Adobe)\n'
    s = s + '  /Ordering (UCS)\n'
    s = s + '  /Supplement 0\n'
    s = s + '>> def\n'
    s = s + '1 begincodespacerange\n'
    s = s + '<0000><FFFF>\n'
    s = s + 'endcodespacerange\n'

    (ranges, singles) = gather_tounicode_ranges(unicode_values)

    bunches = len(ranges) // 100
    remainder = len(ranges) % 100
    for i in range(0, bunches):
        s = s + '100 beginbfrange\n'
        for j in range(0, 100):
            s = s + ranges[100*i + j] + '\n'
        s = s + 'endbfrange\n'
    if remainder != 0:
        s = s + '{0} beginbfrange\n'.format(remainder)
        for j in range(0, remainder):
            s = s + ranges[100*bunches + j] + '\n'
        s = s + 'endbfrange\n'

    bunches = len(singles) // 100
    remainder = len(singles) % 100
    for i in range(0, bunches):
        s = s + '100 beginbfchar\n'
        for j in range(0, 100):
            s = s + singles[100*i + j] + '\n'
        s = s + 'endbfchar\n'
    if remainder != 0:
        s = s + '{0} beginbfchar\n'.format(remainder)
        for j in range(0, remainder):
            s = s + singles[100*bunches + j] + '\n'
        s = s + 'endbfchar\n'

    s = s + 'endcmap\n'
    s = s + 'CMapName currentdict /CMap defineresource pop\n'
    s = s + 'end\n'
    s = s + 'end'
    return s

#--------------------------------------------------------------------------

def make_encrypt_dict(id_string):

    encrypt = None

    if pdf_crypto.crypto_params.encryption_algorithm != 0:
        the_O_field = pdf_crypto.compute_O_field()
        pdf_crypto.compute_encryption_key(passphrase = pdf_crypto.crypto_params.user_passphrase,
                                          the_O_field = the_O_field,
                                          id_string = id_string)
        the_U_field = pdf_crypto.compute_U_field(id_string)

        if pdf_crypto.crypto_params.encryption_algorithm == 2:
            encrypt = { 'Filter' : pdfobj.pdf_name('Standard'),
                        'V'      : pdfobj.pdf_int(pdf_crypto.crypto_params.encryption_algorithm),
                        'Length' : pdfobj.pdf_int(pdf_crypto.crypto_params.key_length),
                        'R'      : pdfobj.pdf_int(pdf_crypto.crypto_params.security_handler),
                        'O'      : pdfobj.pdf_hex_string(the_O_field, encryption_allowed = False),
                        'U'      : pdfobj.pdf_hex_string(the_U_field, encryption_allowed = False),
                        'P'      : pdfobj.pdf_int(pdf_crypto.crypto_params.permissions) }

        elif pdf_crypto.crypto_params.encryption_algorithm == 4:
            std_cf = { 'Type'   : pdfobj.pdf_name('CryptFilter'),
                       'Length' : pdfobj.pdf_int(pdf_crypto.crypto_params.key_length) }
            if pdf_crypto.crypto_params.cipher == 'AES':
                std_cf['CFM'] = pdfobj.pdf_name('AESV2')
            else:
                std_cf['CFM'] = pdfobj.pdf_name('V2')

            encrypt = { 'Filter' : pdfobj.pdf_name('Standard'),
                        'V'      : pdfobj.pdf_int(pdf_crypto.crypto_params.encryption_algorithm),
                        'Length' : pdfobj.pdf_int(pdf_crypto.crypto_params.key_length),
                        'CF'     : pdfobj.pdf_dict({ 'StdCF' : pdfobj.pdf_dict(std_cf) }),
                        'StmF'   : pdfobj.pdf_name('StdCF'),
                        'StrF'   : pdfobj.pdf_name('StdCF'),
                        'R'      : pdfobj.pdf_int(pdf_crypto.crypto_params.security_handler),
                        'O'      : pdfobj.pdf_hex_string(the_O_field, encryption_allowed = False),
                        'U'      : pdfobj.pdf_hex_string(the_U_field, encryption_allowed = False),
                        'P'      : pdfobj.pdf_int(pdf_crypto.crypto_params.permissions) }
            if not pdf_crypto.crypto_params.metadata_encrypted:
                encrypt['EncryptMetadata'] = pdfobj.pdf_bool(False)

    return encrypt

#--------------------------------------------------------------------------

def clear_bit(flags, n, condition = True):
    if condition:
        flags = flags & ~(1 << (n - 1))
    return flags

def permissions_flags(printing = False,
                      modifying = False,
                      copying = False,
                      fill_forms = False,
                      fill_forms_restricted = False,
                      accessibility = False,
                      assembly = False,
                      print_nicely = False):

    flags = -1
    flags = clear_bit(flags, 1)
    flags = clear_bit(flags, 2)
    flags = clear_bit(flags, 3, not printing)
    flags = clear_bit(flags, 4, not modifying)
    flags = clear_bit(flags, 5, not copying)
    flags = clear_bit(flags, 6, not fill_forms)
    flags = clear_bit(flags, 9, not fill_forms_restricted)
    flags = clear_bit(flags, 10, not accessibility)
    flags = clear_bit(flags, 11, not assembly)
    flags = clear_bit(flags, 12, not print_nicely)
    return flags

def decode_permissions(permissions):

    printing = ('p' in permissions)
    degraded_printing = ('P' in permissions)
    if degraded_printing:
        printing = True

    modifying = ('m' in permissions)

    fill_forms = ('f' in permissions)
    fill_forms_restricted = ('F' in permissions)
    if fill_forms_restricted:
        fill_forms = False

    copying = ('c' in permissions)
    accessibility = ('a' in permissions)
    if copying:
        accessibility = True

    assembly = ('A' in permissions)
    
    return permissions_flags(printing = printing,
                             modifying = modifying,
                             copying = copying,
                             fill_forms = fill_forms,
                             fill_forms_restricted = fill_forms_restricted,
                             accessibility = accessibility,
                             assembly = assembly,
                             print_nicely = not degraded_printing)

#--------------------------------------------------------------------------
